package com.atguigu.gmall.product.service.impl;

import com.atguigu.gmall.product.service.TestService;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class TestServiceImpl implements TestService {

    private StringRedisTemplate stringRedisTemplate;
    private RedissonClient redissonClient;

    @Autowired
    public TestServiceImpl(StringRedisTemplate stringRedisTemplate, RedissonClient redissonClient) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.redissonClient = redissonClient;
    }

    /*
        读写锁
            分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态
            测试互斥性(同时允许多个读锁存在但只允许一个写锁存在):
                读   读   => 多个线程同时读,可以发现都可以读取到,不互斥
                读   写   => 当一个线程读时,另一个写的线程必须等到读线程完成才可以进行写操作,互斥
                写   读   => 当一个线程写时,另一个读的线程必须等到写线程完成才可以进行读操作,互斥
                写   写   => 当一个线程写时,另一个写的线程必须等到写线程完成才可以进行写操作,互斥

                结论: 只要涉及到写操作,就必须互斥
     */
    //读写锁(读)
    @Override
    public String readLock() {
        // 初始化锁
        RReadWriteLock readWriteLock = this.redissonClient.getReadWriteLock("read-write-lock");

        // 获取读锁
        RLock rLock = readWriteLock.readLock();

        // 加锁
        rLock.lock(10, TimeUnit.SECONDS);

        // 读取数据
        String msg = this.stringRedisTemplate.opsForValue().get("msg");

        // 释放锁
//        rLock.unlock();

        return msg;
    }

    //读写锁(写)
    @Override
    public String writeLock() {
        // 初始化锁
        RReadWriteLock readWriteLock = this.redissonClient.getReadWriteLock("read-write-lock");

        // 获取写锁
        RLock wLock = readWriteLock.writeLock();

        // 加锁
//        wLock.lock();
        wLock.lock(10, TimeUnit.SECONDS);
//        try {
//            boolean flag = wLock.tryLock(100, 10, TimeUnit.SECONDS);
//        } catch (InterruptedException e) {
//            throw new RuntimeException(e);
//        }

        // 写入到 redis
        this.stringRedisTemplate.opsForValue().set("msg", String.valueOf(System.currentTimeMillis()));

        // 释放锁
//        wLock.unlock();

        return "写入了一条数据到 redis 中";
    }


    /*
        Redisson 的使用步骤
            (1) 将 RedissonClient 对象注入到使用的类中
            (2) 获取锁
                 RLock lock = this.redissonClient.getLock();
                 lock.lock();
            (3) 释放锁
                lock.unlock();
     */
    @Override
    public void testLock() {

        // 定义 key,模拟 sku 查询
        String lockSku = "sku:" + 1314 + ":info";
        // 获取锁
        RLock lock = this.redissonClient.getLock(lockSku);

        // 加锁
//        lock.lock();
        // 加锁并设置超时时间
//        lock.lock(7, TimeUnit.SECONDS);

        try {
            // 尝试加锁,最多等待 100 秒,上锁以后 10 秒自动解锁
            boolean flag = lock.tryLock(100, 10, TimeUnit.SECONDS);
            if (flag) {
                // 从 redis 中查询 num 数据
                String value = this.stringRedisTemplate.opsForValue().get("num");
                // 判断是否为空
                if (StringUtils.isBlank(value)) {
                    return;
                }
                // 对 num 数据 +1 处理
                int num = Integer.parseInt(value);
                // 存储到 redis
                this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
            } else {
                // 自旋
                Thread.sleep(50);
                this.testLock();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    /*
        测试本地锁的局限性
        实现步骤:
            (1)从 redis 中获取 num 数据(在 redis 中设置 num 的初始值为 0)
            (2)对 num 数据进行 +1
            (3)设置到 redis 中

        使用 ab 工具测试
            ab 是 apachebench 命令的缩写
            ab 是一个 http 自带很好用的压力测试工具, ab 命令会创建多个并发访问线程,模拟多个访问者同时对某一 URL 地址进行访问,可以用来测试应用的负载压力,
            也可以用来测试 nginx、tomcat 等其它 Web 服务器的压力负载性能

            使用 ab 测试工具: httpd-tools => yum install -y httpd-tools
            命令:
                ab  -n (一次发送的请求数)  -c (请求的并发数) 访问路径

            测试: 5000 请求, 100 并发
                ab  -n 5000 -c 100 http://192.168.1.107:8206/admin/product/test/testLock

            测试结果: 5000 次请求的值并不是 5000,而是一个不确定的值,比如本次请求后结果为 161
            原因:
                比如 redis 初始值为 0,同时被多个线程获取,这些线程会重复的将 redis 中的值变为 1

            解决方式一:
                添加 synchronized 关键字, synchronized 的安全控制锁是由 jvm 进程进行管理
                存在问题:
                    (1)访问效率低,可以明显发现添加了 synchronized 关键字后,该方法的响应时间变长
                    (2)在集群情况下, synchronized 无效
                        原因:
                            synchronized 只能给当前应用加锁,对于其他环境无效
                            比如 redis 初始值为 0,此时请求同时分发到多个应用,这些应用开始获取的值可能都是 0,然后重复修改为了 1
                            所以本地锁只能锁住同一工程内的资源,在分布式系统里面都存在局限性
                            需要一种跨 JVM 的互斥机制来控制共享资源的访问,也就是分布式锁
            解决方式二:
                 基于缓存 Redis
                 原理:
                    其实核心思想就是 '占坑',在 redis 中有一个 SETNX 命令,即将 key 的值设为 value,当且仅当 key 不存在,若 key 已经存在,则不进行任何动作
                    所以在集群环境下,每一个应用首先尝试使用 SETNX 向 redis 中设置值,如果能设置成功,则进行从 redis 中获取值取并 +1 的动作,操作完成后要删除
                    掉 redis 中用于占坑的锁,如果不能设置成功,则进行自旋操作,也就是再次调用自己,直到能获取到锁为止
                步骤:
                    1.获取数据前,先获取锁
                    2.获取到了锁,才能进行操作
                    3.操作完成后一定要释放锁
                存在问题:
                    SETNX 可能刚好获取到锁,此时业务逻辑出现异常,导致锁无法释放
                解决方案:
                    设置超时时间
                        方案一:
                            Boolean lock = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
                            this.stringRedisTemplate.expire("lock",7, TimeUnit.SECONDS);
                            存在问题:
                                当前先获取锁,再设置超时时间不是原子性操作,如果在 setnx 和 expire 之间出现异常,锁也无法释放
                        方案二:
                             Redis 2.6.12 版本开始进行了增强
                              SET 命令可以通过参数来修改
                                EX: 设置键的过期时间为 second 秒, SET key value EX second 效果等同于 SETEX key second value
                                PX: 设置键的过期时间为 millisecond 毫秒, SET key value PX millisecond 效果等同于 PSETEX key millisecond value
                                NX: 只在键不存在时,才对键进行设置操作, SET key value NX 效果等同于 SETNX key value
                                XX: 只在键已经存在时,才对键进行设置操作

                             Boolean lock = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", "111", 7, TimeUnit.SECONDS);

                            存在问题:
                                超时时间的设置,如果程序执行的过慢,导致锁的提前释放
                                比如有三个线程同时请求该方法,其中 1 线程由于程序问题虽然锁的失效时间是 3 秒,但是程序执行了 7 秒,那么锁就自动失效了,
                                此时 2 线程就能成功添加锁并执行该方法,但 1 线程执行完毕后同样会去执行释放锁的操作,此时 1 线程释放的其实是 2 线程的
                                锁,此时 3 线程又可以进来并执行该方法了,以此类推,如果设置了锁的超时时间,在程序执行过程中,其中某一个线程超时被自动释放
                                了锁,就会导致后续锁的混乱释放,结果致使这个程序中相当于没有锁的控制
                            解决思路:
                                UUID 方案解决
                                   (1)在获取锁之前,获取一个 UUID 唯一值,目的是在获取锁时,添加标识
                                   (2)在获取锁时,作为 value 存储
                                   (3)在释放锁时,判断本地的 UUID 和存储到锁对应的值的 UUID 是否一致
                                代码:
                                            String uuid = UUID.randomUUID().toString().replaceAll("-", "");
                                            Boolean lock = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 7, TimeUnit.SECONDS);

                                            String lockValue = this.stringRedisTemplate.opsForValue().get("lock");
                                            // 只有确认了是自己的锁时才进行释放
                                            if (uuid.equals(lockValue)) {
                                                this.stringRedisTemplate.delete("lock");
                                            }
                                存在问题:
                                    判断锁是否是自己的和释放锁之间不是原子性的操作,假设线程 1 执行到判断锁是否是自己的,此时 redis 中的锁没有过期,所以可以得到结果为
                                    ture,即么它将会执行释放锁的操作,此时正巧 redis 中的锁过期了,并且线程 2 开始执行向 redis 中加入了自己的锁,此时线程 1 重新抢回
                                    CPU 的执行权,这时候线程 1 删除的锁将会是线程 2 的
                                解决方案:
                                    使用 lua 脚本实现原子操作
     */
    public void testLock2() {
        // 使用 SETNX 命令,从 redis 中获取锁
//        Boolean lock = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
        // 设置超时时间
//        this.stringRedisTemplate.expire("lock",7, TimeUnit.SECONDS);

        // 生成 UUID
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        Boolean lock = this.stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 7, TimeUnit.SECONDS);
        // 获取到了锁
        if (lock) {
            // 从 redis 中查询 num 数据
            String value = this.stringRedisTemplate.opsForValue().get("num");
            // 判断是否为空
            if (StringUtils.isBlank(value)) {
                return;
            }
            // 对 num 数据 +1 处理
            int num = Integer.parseInt(value);
            // 存储到 redis
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            // 存在问题: 判断锁和删除锁非原子性操作
//            // 获取当前的锁值
//            String lockValue = this.stringRedisTemplate.opsForValue().get("lock");
//            // 只有确认了是自己的锁时才进行释放
//            if (uuid.equals(lockValue)) {
//                // 释放锁
//                this.stringRedisTemplate.delete("lock");
//            }

            // 定义 lua 脚本: http://doc.redisfans.com/string/set.html
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            // 创建脚本对象
            DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
            // 设置脚本
            defaultRedisScript.setScriptText(script);
            // 设置返回值类型
            defaultRedisScript.setResultType(Long.class);

            // 执行删除
            this.stringRedisTemplate.execute(defaultRedisScript, Arrays.asList("lock"), uuid);
        } else {
            // 没有获取到锁
            try {
                // 睡眠 100ms,防止速度太快,造成压力过大
                Thread.sleep(100);
                // 再次进行调用
                this.testLock();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    // 本地锁的演示
    public synchronized void testLock1() {
        // 从 redis 中查询 num 数据
        String value = this.stringRedisTemplate.opsForValue().get("num");
        // 判断是否为空
        if (StringUtils.isBlank(value)) {
            return;
        }
        // 对 num 数据 +1 处理
        int num = Integer.parseInt(value);
        // 存储到 redis
        this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
    }
}
